makeRegistryInternal = function(id, file.dir, sharding, work.dir, multiple.result.files, seed, packages, src.dirs=character(0L)) {
  checkArg(id, cl = "character", len = 1L, na.ok = FALSE)
  checkIdValid(id, allow.minus=FALSE)
  checkArg(file.dir, cl = "character", len = 1L, na.ok = FALSE)
  checkArg(sharding, cl = "logical", len = 1L, na.ok = FALSE)
  if (missing(work.dir))
    work.dir = getwd()
  checkArg(work.dir, cl = "character", len = 1L, na.ok = FALSE)
  checkArg(multiple.result.files, cl = "logical", len = 1L, na.ok = FALSE)

  if (missing(seed)) {
    seed = getRandomSeed()
  } else {
    seed = convertInteger(seed)
    checkArg(seed, cl = "integer", len = 1L, lower = 1L, na.ok = FALSE)
  }
  checkArg(packages, cl = "character", na.ok = FALSE)
  packages = union(packages, "BatchJobs")
  requirePackages(packages, stop=TRUE, suppress.warnings=TRUE)
  checkArg(src.dirs, cl = "character", na.ok = FALSE)
  sourceRegistryFilesInternal(work.dir, src.dirs, .GlobalEnv)

  # make paths absolute to be sure. otherwise cfSSH wont work for example
  checkDir(file.dir, create=TRUE, check.empty=TRUE, check.posix=TRUE, msg=TRUE)
  file.dir = makePathAbsolute(file.dir)
  job.dir = getJobParentDir(file.dir)
  checkDir(job.dir, create=TRUE, check.empty=TRUE)
  fun.dir = getFunDir(file.dir)
  checkDir(fun.dir, create=TRUE, check.empty=TRUE)
  checkDir(getResourcesDir(file.dir), create=TRUE, check.empty=TRUE)
  checkDir(getPendingDir(file.dir), create=TRUE, check.empty=TRUE)
  checkDir(getExportDir(file.dir), create=TRUE, check.empty=TRUE)
  checkDir(work.dir, check.posix=TRUE)
  work.dir = makePathAbsolute(work.dir)

  packages = setNames(lapply(packages, function(pkg) list(version = packageVersion(pkg))), packages)

  setClasses(list(
    id = id,
    version = R.version,
    RNGkind = RNGkind(),
    db.driver = "SQLite",
    db.options = list(),
    seed = seed,
    file.dir = file.dir,
    sharding = sharding,
    work.dir = work.dir,
    src.dirs = src.dirs,
    multiple.result.files = multiple.result.files,
    packages = packages[order(names(packages))]
  ), "Registry")
}


#' Construct a registry object.
#'
#' Note that if you don't want links in your paths (\code{file.dir}, \code{work.dir}) to get resolved and have
#' complete control over the way the path is used internally, pass an absolute path which begins with \dQuote{/}.
#'
#' Every object is a list that contains the passed arguments of the constructor.
#
#' @param id [\code{character(1)}]\cr
#'   Name of registry. Displayed e.g. in mails or in cluster queue.
#' @param file.dir [\code{character(1)}]\cr
#'   Path where files regarding the registry / jobs should be saved.
#'   Default is \dQuote{<id>-files} in current working directory if \code{id} is set.
#' @param sharding [\code{logical(1)}]\cr
#'   Enable sharding to distribute result files into different subdirectories?
#'   Important if you have many experiments.
#'   Default is \code{TRUE}.
#' @param work.dir [\code{character(1)}]\cr
#'   Working directory for R process when experiment is executed.
#'   Default is the current working directory when registry is created.
#' @param multiple.result.files [\code{logical(1)}]\cr
#'   Should a result file be generated for every list element of the
#'   returned list of the job function?
#'   Note that the function provided to \code{\link{batchMap}} or
#'   \code{\link{batchReduce}} must return a named list if this is set to \code{TRUE}.
#'   The result file will be named \dQuote{<id>-result-<element name>.RData}
#'   instead of \dQuote{<id>-result.RData}.
#'   Default is \code{FALSE}.
#' @param seed [\code{integer(1)}]\cr
#'   Start seed for experiments. The first experiment in the registry will use this
#'   seed, for the subsequent ones the seed is incremented by 1.
#'   Default is a random number from 1 to \code{.Machine$integer.max/2}.
#' @param packages [\code{character}]\cr
#'   Packages that will always be loaded on each node.
#'   Default is \code{character(0)}.
#' @param src.dirs [\code{character}]\cr
#'   Directories relative to your \code{work.dir} containing R scripts 
#'   to be sourced on registry load (both on slave and master).
#'   Files not matching the pattern \dQuote{\\.[Rr]$} are ignored.
#'   Useful if you have many helper functions that are needed during the execution of your jobs.
#'   These files should only contain function definitions and no executable code.
#'   Default is \code{character(0)}.
#' @param skip [\code{logical(1)}]\cr
#'   Skip creation of a new registry if a registry is found in \code{file.dir}.
#'   Defaults to \code{TRUE}.
#' @return [\code{\link{Registry}}]
#' @aliases Registry
#' @export
#' @examples
#' reg <- makeRegistry(id="BatchJobsExample", file.dir=tempfile(), seed=123)
#' print(reg)
makeRegistry = function(id, file.dir, sharding=TRUE, work.dir, multiple.result.files = FALSE,
                        seed, packages=character(0L), src.dirs=character(0L), skip = TRUE) {
  if (missing(file.dir))
    file.dir = file.path(getwd(), paste(id, "files", sep="-"))
  checkArg(skip, "logical", len=1L, na.ok=FALSE)
  if (skip && isRegistryDir(file.dir))
    return(loadRegistry(file.dir = file.dir))

  reg = makeRegistryInternal(id, file.dir, sharding, work.dir, multiple.result.files, seed, packages, src.dirs)

  dbCreateJobStatusTable(reg)
  dbCreateJobDefTable(reg)
  saveRegistry(reg)
  reg
}

#' @S3method print Registry
print.Registry = function(x, ...) {
  cat("Job registry: ",  x$id, "\n")
  cat("  Number of jobs: ", dbGetJobCount(x), "\n")
  cat("  Files dir:", x$file.dir, "\n")
  cat("  Work dir:", x$work.dir, "\n")
  cat("  Multiple result files:", x$multiple.result.files, "\n")
  cat("  Seed:", x$seed, "\n")
  cat("  Required packages:", paste(names(x$packages), collapse=", "), "\n")
}

#' Load a previously saved registry.
#' @param file.dir [\code{character(1)}]\cr
#'   Location of the file.dir to load the registry from.
#' @param work.dir [\code{character(1)}]\cr
#'   Location of the work. Unchanged if missing.
#' @return [\code{\link{Registry}}].
#' @export
loadRegistry = function(file.dir, work.dir) {
  fn = getRegistryFilePath(file.dir)
  if (!file.exists(fn))
    stopf("No registry found in '%s'", file.dir)
  message("Loading registry: ", fn)
  reg = load2(fn, "reg")

  requirePackages(names(reg$packages), why=sprintf("registry %s", reg$id))

  if (!isOnSlave()) {
    # FIXME check that no jobs are running, if possible, before updating
    adjusted = adjustRegistryPaths(reg, file.dir, work.dir)
    if (!isFALSE(adjusted))
      reg = adjusted

    updated = updateRegistry(reg)
    if (!isFALSE(updated))
      reg = updated

    if (!isFALSE(adjusted) || !isFALSE(updated))
      saveRegistry(reg)
  } else {
    loadExports(reg)
  }
  
  sourceRegistryFilesInternal(reg$work.dir, reg$src.dirs)

  return(reg)
}

saveRegistry = function(reg) {
  fn = getRegistryFilePath(reg$file.dir)
  message("Saving registry: ", fn)
  save(file=fn, reg)
}

isRegistryDir = function(dir) {
  isDirectory(dir) && file.exists(getRegistryFilePath(dir))
}

checkRegistry = function(reg, strict = FALSE) {
  cl = class(reg)
  expected = "Registry"
  if (strict) {
    if (head(cl, 1L) != expected)
      stopf("Registry class mismatch: Expected argument with first class '%s'", expected)
  } else {
    if (expected %nin% cl)
      stopf("Registry class mismatch: Expected argument of class '%s'", expected)
  }
  invisible(TRUE)
}
